//*****************************************************************************
// Copyright 2018 NVIDIA Corporation. All rights reserved.
//*****************************************************************************
/// \file       mi/base/condition.h
/// \brief      Multithreading condition.
///
/// See \ref mi_base_threads.
///
//*****************************************************************************

#ifndef MI_BASE_CONDITION_H
#define MI_BASE_CONDITION_H

#include <mi/base/config.h>
#include <mi/base/types.h>

#ifndef MI_PLATFORM_WINDOWS
#include <cerrno>
#include <pthread.h>
#include <sys/time.h>
#else
#include <mi/base/miwindows.h>
#endif

namespace mi
{

namespace base
{

/** \addtogroup mi_base_threads
@{
*/

/// Conditions allow threads to signal an event and to wait for such a signal, respectively.
class Condition
{
public:
  /// Constructor
  Condition()
  {
#ifndef MI_PLATFORM_WINDOWS
    m_signaled = false;
    pthread_mutex_init(&m_mutex, NULL);
    pthread_cond_init(&m_condvar, NULL);
#else
    m_handle = CreateEvent(NULL, false, false, NULL);
#endif
  }

  /// Destructor
  ~Condition()
  {
#ifndef MI_PLATFORM_WINDOWS
    pthread_mutex_destroy(&m_mutex);
    pthread_cond_destroy(&m_condvar);
#else
    CloseHandle(m_handle);
#endif
  }

  /// Waits for the condition to be signaled.
  ///
  /// If the condition is already signaled at this time the call will return immediately.
  void wait()
  {
#ifndef MI_PLATFORM_WINDOWS
    pthread_mutex_lock(&m_mutex);
    while (!m_signaled)
      pthread_cond_wait(&m_condvar, &m_mutex);
    m_signaled = false;
    pthread_mutex_unlock(&m_mutex);
#else
    WaitForSingleObject(m_handle, INFINITE);
#endif
  }

  /// Waits for the condition to be signaled until a given timeout.
  ///
  /// If the condition is already signaled at this time the call will return immediately.
  ///
  /// \param timeout    Maximum time period (in seconds) to wait for the condition to be signaled.
  /// \return           \c true if the timeout was hit, and \c false if the condition was
  ///                   signaled.
  bool timed_wait(Float64 timeout)
  {
#ifndef MI_PLATFORM_WINDOWS
    struct timeval now;
    gettimeofday(&now, NULL);
    struct timespec timeout_abs;
    timeout_abs.tv_sec = now.tv_sec + static_cast<long>(floor(timeout));
    timeout_abs.tv_nsec = 1000 * now.tv_usec + static_cast<long>(1E9 * (timeout - floor(timeout)));
    if (timeout_abs.tv_nsec > 1000000000)
    {
      timeout_abs.tv_sec += 1;
      timeout_abs.tv_nsec -= 1000000000;
    }

    bool timed_out = false;
    pthread_mutex_lock(&m_mutex);
    while (!m_signaled)
    {
      int result = pthread_cond_timedwait(&m_condvar, &m_mutex, &timeout_abs);
      timed_out = result == ETIMEDOUT;
      if (result != EINTR)
        break;
    }
    m_signaled = false;
    pthread_mutex_unlock(&m_mutex);
    return timed_out;
#else
    DWORD timeout_ms = static_cast<DWORD>(1000 * timeout);
    DWORD result = WaitForSingleObject(m_handle, timeout_ms);
    return result == WAIT_TIMEOUT;
#endif
  }

  /// Signals the condition.
  ///
  /// This will wake up one thread waiting for the condition. It does not matter if the call to
  /// #signal() or #wait() comes first.
  ///
  /// \note If there are two or more calls to #signal() without a call to #wait() in between (and
  /// no outstanding #wait() call), all calls to #signal() except the first one are ignored, i.e.,
  /// calls to #signal() do not increment some counter, but just set a flag.
  void signal()
  {
#ifndef MI_PLATFORM_WINDOWS
    pthread_mutex_lock(&m_mutex);
    m_signaled = true;
    pthread_cond_signal(&m_condvar);
    pthread_mutex_unlock(&m_mutex);
#else
    SetEvent(m_handle);
#endif
  }

  /// Resets the condition.
  ///
  /// This will undo the effect of a #signal() call if there was no outstanding #wait() call.
  void reset()
  {
#ifndef MI_PLATFORM_WINDOWS
    pthread_mutex_lock(&m_mutex);
    m_signaled = false;
    pthread_mutex_unlock(&m_mutex);
#else
    ResetEvent(m_handle);
#endif
  }

private:
#ifndef MI_PLATFORM_WINDOWS
  /// The mutex to be used to protect the m_signaled variable.
  pthread_mutex_t m_mutex;
  /// The condition used to let the thread sleep.
  pthread_cond_t m_condvar;
  /// A variable storing the signaled state.
  bool m_signaled;
#else
  /// The event handle
  HANDLE m_handle;
#endif
};

/*@}*/ // end group mi_base_threads

} // namespace base

} // namespace mi

#endif // MI_BASE_CONDITION_H
